/*
  需求：给整个页面添加背景水印。

  思路：
    1、使用 canvas 特性生成 base64 格式的图片文件，设置其字体大小，颜色等。
    2、将其设置为背景图片，从而实现页面或组件水印效果
  
  使用：设置水印文案，颜色，字体大小即可
  <div v-waterMarker="{text:'版权所有',textColor:'rgba(180, 180, 180, 0.4)'}"></div>
*/

import type { CSSProperties, Directive } from "vue";

import stores from "@/stores";

let observer: MutationObserver;
let darkObserver: MutationObserver;
let stopObserver = false;

const FontGap = 3;

const prepareCanvas: (
  width: number,
  height: number,
  ratio?: number
) => [ctx: CanvasRenderingContext2D, canvas: HTMLCanvasElement, realWidth: number, realHeight: number] = (
  width,
  height,
  ratio = 1
) => {
  const canvas = document.createElement("canvas");
  const ctx = canvas.getContext("2d")!;
  const realWidth = width * ratio;
  const realHeight = height * ratio;
  canvas.setAttribute("width", `${realWidth}px`);
  canvas.setAttribute("height", `${realHeight}px`);
  ctx.save();

  return [ctx, canvas, realWidth, realHeight];
};

const getClips: (
  content: string | HTMLImageElement,
  rotate: number,
  ratio: number,
  width: number,
  height: number,
  font: {
    color: string;
    fontSize: number | string;
    fontWeight: "normal" | "light" | "weight" | number;
    fontStyle: "none" | "normal" | "italic" | "oblique";
    fontFamily: string;
    textAlign: "start" | "end" | "left" | "right" | "center";
    textBaseline: "top" | "hanging" | "middle" | "alphabetic" | "ideographic" | "bottom";
  },
  gapX: number,
  gapY: number
) => [dataURL: string, finalWidth: number, finalHeight: number] = (content, rotate, ratio, width, height, font, gapX, gapY) => {
  const [ctx, canvas, contentWidth, contentHeight] = prepareCanvas(width, height, ratio);

  if (content instanceof HTMLImageElement) {
    // Image
    ctx.drawImage(content, 0, 0, contentWidth, contentHeight);
  } else {
    // Text
    const { color, fontSize, fontStyle, fontWeight, fontFamily, textAlign, textBaseline } = font;
    const mergedFontSize = Number(fontSize) * ratio;

    ctx.font = `${fontStyle} normal ${fontWeight} ${mergedFontSize}px/${height}px ${fontFamily}`;
    ctx.fillStyle = color;
    ctx.textAlign = textAlign;
    ctx.textBaseline = textBaseline;
    const contents = Array.isArray(content) ? content : [content];
    contents?.forEach((item, index) => {
      ctx.fillText(item ?? "", contentWidth / 2, index * (mergedFontSize + FontGap * ratio));
    });
  }

  // ==================== Rotate ====================
  const angle = (Math.PI / 180) * Number(rotate);
  const maxSize = Math.max(width, height);
  const [rCtx, rCanvas, realMaxSize] = prepareCanvas(maxSize, maxSize, ratio);

  // Copy from `ctx` and rotate
  rCtx.translate(realMaxSize / 2, realMaxSize / 2);
  rCtx.rotate(angle);
  if (contentWidth > 0 && contentHeight > 0) {
    rCtx.drawImage(canvas, -contentWidth / 2, -contentHeight / 2);
  }

  // Get boundary of rotated text
  function getRotatePos(x: number, y: number) {
    const targetX = x * Math.cos(angle) - y * Math.sin(angle);
    const targetY = x * Math.sin(angle) + y * Math.cos(angle);
    return [targetX, targetY];
  }

  let left = 0;
  let right = 0;
  let top = 0;
  let bottom = 0;

  const halfWidth = contentWidth / 2;
  const halfHeight = contentHeight / 2;
  const points = [
    [0 - halfWidth, 0 - halfHeight],
    [0 + halfWidth, 0 - halfHeight],
    [0 + halfWidth, 0 + halfHeight],
    [0 - halfWidth, 0 + halfHeight]
  ];
  points.forEach(([x, y]) => {
    const [targetX, targetY] = getRotatePos(x, y);
    left = Math.min(left, targetX);
    right = Math.max(right, targetX);
    top = Math.min(top, targetY);
    bottom = Math.max(bottom, targetY);
  });

  const cutLeft = left + realMaxSize / 2;
  const cutTop = top + realMaxSize / 2;
  const cutWidth = right - left;
  const cutHeight = bottom - top;

  // ================ Fill Alternate ================
  const realGapX = gapX * ratio;
  const realGapY = gapY * ratio;
  const filledWidth = (cutWidth + realGapX) * 2;
  const filledHeight = cutHeight + realGapY;

  const [fCtx, fCanvas] = prepareCanvas(filledWidth, filledHeight);

  function drawImg(targetX = 0, targetY = 0) {
    fCtx.drawImage(rCanvas, cutLeft, cutTop, cutWidth, cutHeight, targetX, targetY, cutWidth, cutHeight);
  }
  drawImg();
  drawImg(cutWidth + realGapX, -cutHeight / 2 - realGapY / 2);
  drawImg(cutWidth + realGapX, +cutHeight / 2 + realGapY / 2);

  return [fCanvas.toDataURL(), filledWidth / ratio, filledHeight / ratio];
};

const waterMarker: Directive = {
  mounted(el, binding) {
    if (!!binding.value?.hide) return

    const { isDark } = stores.state.value?.["geeker-global"] ?? {};
    const { userInfo } = stores.state.value?.["geeker-user"] ?? {};

    let color = binding.value?.color ?? isDark ? "rgba(255,255,255,0.03)" : "rgba(0,0,0,0.08)";

    const canvas = document.createElement("canvas");
    const ctx = canvas.getContext("2d");

    const image = binding.value?.image;
    const content = binding.value?.content ?? userInfo?.name ?? "MERAK2.0";

    const ratio = window.devicePixelRatio || 1;
    const rotate = binding.value?.rotate ?? -22;

    const fontSize = binding.value?.fontSize ?? 14;
    const fontWeight = binding.value?.fontWeight ?? "normal";
    const fontStyle = binding.value?.fontStyle ?? "normal";
    const fontFamily =
      binding.value?.fontFamily ??
      '-apple-system,BlinkMacSystemFont,"Segoe UI",Helvetica,Arial,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Liberation Sans","PingFang SC","Microsoft YaHei","Hiragino Sans GB","Wenquanyi Micro Hei","WenQuanYi Zen Hei","ST Heiti",SimHei,SimSun,"WenQuanYi Zen Hei Sharp",sans-serif';
    const textAlign = binding.value?.textAlign ?? "center";
    const textBaseline = binding.value?.textBaseline ?? "top";

    const gapX = binding.value?.gap?.[0] ?? 200;
    const gapY = binding.value?.gap?.[1] ?? 200;
    const gapXCenter = gapX / 2;
    const gapYCenter = gapY / 2;
    const offsetLeft = binding.value?.offset?.[0] ?? gapXCenter;
    const offsetTop = binding.value?.offset?.[1] ?? gapYCenter;
    // 水印样式
    const markStyle: CSSProperties = {
      position: "absolute",
      top: "20px",
      left: "20px",
      margin: "0",
      width: "calc(100% - 40px)",
      height: "calc(100% - 40px)",
      pointerEvents: "none",
      backgroundRepeat: "repeat",
      zIndex: (binding.value?.zIndex ?? 9).toString()
    };
    let positionLeft = offsetLeft - gapXCenter;
    let positionTop = offsetTop - gapYCenter;
    if (positionLeft > 0) {
      markStyle.left = `${positionLeft + 20}px`;
      markStyle.width = `calc(100% - ${positionLeft + 40}px)`;
      positionLeft = 0;
    }
    if (positionTop > 0) {
      markStyle.top = `${positionTop + 20}px`;
      markStyle.height = `calc(100% - ${positionTop + 40}px)`;
      positionTop = 0;
    }
    markStyle.backgroundPosition = `${positionLeft}px ${positionTop}px`;
    // 水印宽高
    let width = binding.value?.width ?? 120;
    let height = binding.value?.height ?? 64;
    if (!image && !!ctx?.measureText) {
      ctx.font = `${Number(fontSize)}px ${fontFamily}`;
      const metrics = ctx.measureText(content);
      width = metrics.width;
      height =
        metrics.fontBoundingBoxAscent !== undefined
          ? metrics.fontBoundingBoxAscent + metrics.fontBoundingBoxDescent
          : metrics.actualBoundingBoxAscent + metrics.actualBoundingBoxDescent;
    }
    // 绘制水印
    const drawMarker = () => {
      // 绘制图像
      const drawCanvas = (drawContent: string | HTMLImageElement) => {
        const [textClips, clipWidth] = getClips(
          drawContent,
          rotate,
          ratio,
          width,
          height,
          {
            color: color,
            fontSize: fontSize,
            fontStyle: fontStyle,
            fontWeight: fontWeight,
            fontFamily: fontFamily,
            textAlign: textAlign,
            textBaseline: textBaseline
          },
          gapX,
          gapY
        );
        markStyle.backgroundImage = `url('${textClips}')`;
        markStyle.backgroundSize = `${Math.floor(clipWidth)}px`;
      };
      if (image) {
        const img = new Image();
        img.onload = () => {
          drawCanvas(img);
        };
        img.onerror = () => {
          drawCanvas(content);
        };
        img.crossOrigin = "anonymous";
        img.referrerPolicy = "no-referrer";
        img.src = image;
      } else {
        drawCanvas(content);
      }
    };
    // 水印容器
    const markContent = document.createElement("div");
    // 添加水印
    const toLowercaseSeparator = (key: string) => key.replace(/([A-Z])/g, "-$1").toLowerCase();
    const destroyWatermark = () => {
      if (el.contains(markContent)) {
        markContent.remove();
      }
    };
    const addWaterMarker = () => {
      stopObserver = true;
      markContent.setAttribute(
        "style",
        Object.keys(markStyle)
          .map(key => `${toLowercaseSeparator(key)}: ${markStyle[key as keyof CSSProperties]};`)
          .join(" ")
      );
      if (window.getComputedStyle(el).position === "static") {
        el.style.position = "relative";
      }
      el.appendChild(markContent);
      const timer = requestAnimationFrame(() => {
        stopObserver = false;
        cancelAnimationFrame(timer);
      });
    };

    drawMarker();
    destroyWatermark();
    addWaterMarker();

    // 防修改
    observer = new MutationObserver(mutations => {
      if (stopObserver) return;
      for (let mutation of mutations) {
        if (!!mutation.removedNodes.length && Array.from(mutation.removedNodes).includes(markContent)) {
          destroyWatermark();
          addWaterMarker();
        }
        if (mutation.type === "attributes" && mutation.target === markContent) {
          destroyWatermark();
          addWaterMarker();
        }
      }
    });
    observer.observe(el, {
      attributes: true,
      subtree: true,
      childList: true
    });

    // 模式切换
    darkObserver = new MutationObserver(() => {
      if (!!binding.value?.color) return;
      let timer: number | null = null;
      if (!!timer) cancelAnimationFrame(timer);
      timer = requestAnimationFrame(() => {
        const _isDark = document.documentElement.getAttribute("class")?.includes("dark");
        color = _isDark ? "rgba(255,255,255,0.03)" : "rgba(0,0,0,0.08)";
        drawMarker();
        destroyWatermark();
        addWaterMarker();
        if (!!timer) cancelAnimationFrame(timer);
        timer = null;
      });
    });
    darkObserver.observe(document.documentElement, {
      attributes: true
    });
  },
  beforeUnmount() {
    observer.disconnect();
    darkObserver.disconnect();
  }
};

export default waterMarker;
